using System;
using System.Collections.Generic;
using System.Text;

using System.Net;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using System.Runtime.Serialization;

namespace Horizon
{
    /// <summary>
    /// Encapsulation of a library
    /// </summary>
    [Serializable]
    public class Library : IDeserializationCallback
    {
        #region "Private members"

        private string baseUrlValue, nameValue, profileValue, 
            langValue, greetingValue, versionValue, anonSessionIdValue;

        private List<SearchLimit> searchLimitsList = new List<SearchLimit>();
        private List<SortOption> sortOptionsList = new List<SortOption>();
        private List<SearchOption> searchOptionsList = new List<SearchOption>();
        private List<Patron> usersList = new List<Patron>();
        [NonSerialized]
        private HorizonDataMapper mapper;
        [NonSerialized]
        private HorizonDataAccess comm;
        #endregion

        #region "Public Properties"

        /// <summary>
        /// The base URL for all requests to this server.  At this time, it appears that
        /// all Horizon Information Portal URL's end with "/ipac20/ipac.jsp".
        /// </summary>
        public string BaseUrl
        {
            get { return baseUrlValue; }
            set { baseUrlValue = value; }
        }

        /// <summary>
        /// The list of users associated with this library
        /// </summary>
        public List<Patron> Users
        {
            get { return usersList; }
            set { usersList = value; }
        }

        /// <summary>
        /// The descriptive name for this library
        /// </summary>
        public string Name
        {
            get { return nameValue; }
            set { nameValue = value; }
        }

        /// <summary>
        /// The version of Horizon Information Portal
        /// </summary>
        public string HorizonVersion
        {
            get { return versionValue; }
            set { versionValue = value; }
        }

        /// <summary>
        /// Greeting message returned from server (not all support this)
        /// </summary>
        public string Greeting
        {
            get { return greetingValue; }
            set { greetingValue = value; }
        }

        /// <summary>
        /// Profile string for requests.  Apparently multiple profiles are supported,
        /// but no need seen for it yet.  Will be retrieved from initial server request,
        /// or can be manually entered.
        /// </summary>
        public string Profile
        {
            get { return profileValue; }
            set { profileValue = value; }
        }

        /// <summary>
        /// The set of sort options supported by this library
        /// </summary>
        public List<SortOption> SortOptions
        {
            get { return sortOptionsList; }
            set { sortOptionsList = value; }
        }

        /// <summary>
        /// The list of search options supported by this library
        /// </summary>
        public List<SearchOption> SearchOptions
        {
            get { return searchOptionsList; }
            set { searchOptionsList = value; }
        }

        /// <summary>
        /// The list of search limits supported by this library
        /// </summary>
        public List<SearchLimit> SearchLimits
        {
            get { return searchLimitsList; }
            set { searchLimitsList = value; }
        }

        /// <summary>
        /// Session ID associated with non-authenticated requests (such as searches)
        /// </summary>
        public string AnonSessionId
        {
            get { return anonSessionIdValue; }
            set { anonSessionIdValue = value; }
        }

#endregion

        public Library()
        {
            comm = new HorizonDataAccess(this);
            mapper = new HorizonDataMapper();
        }

        /// <summary>
        /// Creates a new library object with the given name and base URL
        /// </summary>
        /// <param name="name"></param>
        /// <param name="baseUrl"></param>
        public Library(string name, string baseUrl) : this()
        {
            baseUrlValue = baseUrl;
            nameValue = name;
        }

        /// <summary>
        /// Reads basic library information such as search options
        /// </summary>
        public void Init()
        {
            comm.CurrentLibrary = this;

            ServerRequest req = new ServerRequest("search", "", "basic_search");
            XmlDocument doc = comm.ServerGET(req, null);

            // Determine current Horizon version
            this.versionValue = doc.SelectSingleNode("searchresponse/version").InnerText;

            // Determine default profile string if not specified
            if (string.IsNullOrEmpty(this.profileValue))
                this.profileValue = doc.SelectSingleNode("searchresponse/profile").InnerText;

            // Determine default language
            this.langValue = doc.SelectSingleNode("searchresponse/lang").InnerText;

            // Determine library greeting
            this.greetingValue = HorizonDataAccess.SafeInnerText(doc, "searchresponse/active_menu/simple/desc");

            // Determine valid searches
            GetSearchTypes(doc.SelectSingleNode("searchresponse/aspectlist"));

            // Determine sort options
            GetSortOptions(doc.SelectSingleNode("searchresponse/predefsortlist"));

            // Determine limits (fiction, children's, Downtown Branch, etc.)
            GetLimits(doc.SelectSingleNode("searchresponse/predefinedlimits/limitbox"));
        }


        /// <summary>
        /// Reads patron information with the account/itemsout message
        /// </summary>
        /// <param name="user">Patron to lookup</param>
        public void ReadPatronDetails(Patron user)
        {
            // This will not work without a session
            if (user.CurrentSession.SessionKey == null) Login(user);

            // Create request to obtain details
            ServerRequest req = new ServerRequest("account", "itemsout", "");
            XmlDocument doc = comm.ServerGET(req, user);

            // Read user details from XML nodes
            user.Name = doc.SelectSingleNode("//security/name").InnerText;
            user.Address = doc.SelectSingleNode("//security/address").InnerText;
            user.EmailAddress = doc.SelectSingleNode("//security/emailaddress").InnerText;
            user.PatronId = doc.SelectSingleNode("//security/patronid").InnerText;
            user.Phone = doc.SelectSingleNode("//security/phone").InnerText;

            user.ItemsLostCount = int.Parse(doc.SelectSingleNode("//itemsoutdata/totallost").InnerText);
            user.ItemsOverdueCount = int.Parse(doc.SelectSingleNode("//itemsoutdata/totaloverdue").InnerText);
            user.RenewalsAllowed = bool.Parse(doc.SelectSingleNode("//itemsoutdata/renewalsallowed").InnerText);

            mapper.UpdateItemsOut(user, doc);

            // TODO: Call only if there are blocks (determined in original login response)
            UpdateBlocks(user);

            // TODO: Call only if there are holds (determined in original login response)
            UpdateHolds(user);
        }


        /// <summary>
        /// Update data regarding current holds for a given patron
        /// </summary>
        /// <param name="user">Patron whose holds should be updated</param>
        public void UpdateHolds(Patron user)
        {
            // This will not work without a session
            if (user.CurrentSession.SessionKey == null) return;

            // Now obtain user data
            ServerRequest req = new ServerRequest("account", "holds", "");
            XmlDocument doc = comm.ServerGET(req, user);

            // Read ready books (holds)
            user.Holds.Clear();
            XmlNodeList readyItemsNodes = doc.SelectNodes("//holdsdata/ready/readyitem");
            foreach (XmlNode node in readyItemsNodes)
            {
                Hold hold = new Hold();

                // Read generic holding information (as available)
                mapper.PopulateStaticHoldingData(hold, node);
                
                hold.DatePlaced = HorizonDataAccess.SafeInnerText(node, "dateplaced");
                hold.DateExpires = HorizonDataAccess.SafeInnerText(node, "dateexpires");
                hold.PickupLocation = HorizonDataAccess.SafeInnerText(node, "pickuploc");

                hold.Patron = user;

                user.Holds.Add(hold);
            }
        }

        /// <summary>
        /// Update data regarding current blocks (overdue, etc) for a given patron
        /// </summary>
        /// <param name="user">Patron whose blocks should be updated</param>

        public void UpdateBlocks(Patron user)
        {
            // This will not work without a session
            if (user.CurrentSession.SessionKey == null) return;

            // Now obtain user data
            ServerRequest req = new ServerRequest("account", "blocks", "");
            XmlDocument doc = comm.ServerGET(req, user);

            // Read check-out books
            user.Blocks.Clear();
            XmlNodeList itemsOut = doc.SelectNodes("//patronpersonalresponse/blockdata/block");
            foreach (XmlNode node in itemsOut)
            {
                Block block = new Block();

                block.Title = HorizonDataAccess.SafeInnerText(node, "title");
                block.BlockReason = HorizonDataAccess.SafeInnerText(node, "reason");
                block.DueDate = HorizonDataAccess.SafeInnerText(node, "duedate");
                block.Amount = HorizonDataAccess.SafeInnerText(node, "amount");

                block.Patron = user;

                user.Blocks.Add(block);
            }
        }


        /// <summary>
        /// Renew a given holding to extend the Due Date
        /// </summary>
        /// <param name="user">Patron to whom holding is checked out</param>
        /// <param name="h">Holding to renewe</param>
        public void RenewHolding(Patron user, Holding h)
        {
            string postData = string.Format("GetXML=true&menu=account&submenu=itemsout&lang=eng&renewitems=Renew&session={0}",
                user.CurrentSession.SessionKey);
            postData += "&renewitemkeys=" + h.BarCode; // Can be repeated for multiple renewals...

            XmlDocument doc = comm.ServerPOST(postData, user);

            mapper.UpdateItemsOut(user, doc);
        }

        /// <summary>
        /// Executes a search request and returns the list of holdings returned by the server
        /// </summary>
        /// <param name="req"></param>
        /// <returns></returns>
        public List<Holding> Search(ServerRequest req)
        {
            XmlDocument doc = comm.ServerGET(req, null);

            // Convert holding information to objects
            List<Holding> holdings = mapper.GetHoldings(doc);

            return holdings;
        }



        /// <summary>
        /// Adds a new SearchLimit object based on the predeflimitentry XML node
        /// </summary>
        /// <param name="searchLimitNode">XML predeflimitentry node </param>
        private void GetLimits(XmlNode searchLimitNode)
        {
            searchLimitsList.Clear();
            if (searchLimitNode == null) return;

            searchLimitsList.Add(new SearchLimit("", "[ All ]"));

            foreach (XmlNode materialType in searchLimitNode.SelectNodes("predeflimitentry"))
            {
                searchLimitsList.Add(new SearchLimit(materialType.SelectSingleNode("limit").InnerText, materialType.SelectSingleNode("label").InnerText));
            }
        }

        /// <summary>
        /// Adds a new SearchOption object based on the aspectentry XML node
        /// </summary>
        /// <param name="searchNode">XML aspectentry node</param>
        private void GetSearchTypes(XmlNode searchNode)
        {
            searchOptionsList.Clear();
            if (searchNode == null) return;

            foreach (XmlNode aspectEntry in searchNode.SelectNodes("aspectentry"))
            {
                searchOptionsList.Add(new SearchOption(aspectEntry.SelectSingleNode("aspectlabel").InnerText, aspectEntry.SelectSingleNode("shortcut").InnerText));
            }
        }

        /// <summary>
        /// Adds a new SortOption object based on the sortentry XML node
        /// </summary>
        /// <param name="sortNode">XML sortentry node</param>
        private void GetSortOptions(XmlNode sortNode)
        {
            sortOptionsList.Clear();
            if (sortNode == null) return;

            foreach (XmlNode aspectEntry in sortNode.SelectNodes("sortentry"))
            {
                sortOptionsList.Add(new SortOption(aspectEntry.SelectSingleNode("label").InnerText, aspectEntry.SelectSingleNode("sortview").InnerText));
            }
        }


        /// <summary>
        /// This will grab the login form, extract the session ID, then
        /// authenticate the user.  It will also create/populate the current
        /// HorizonUser object with all returned details.
        /// With help from: http://blog.danbartels.com/archive/2004/09/06/162.aspx
        /// </summary>
        public void Login(Patron user)
        {
            if (user == null || string.IsNullOrEmpty(user.BarCode)) throw new Exception("Invalid user");

            if (user.CurrentSession == null) user.CurrentSession = new HorizonSession();

            user.CurrentSession.Invalidate();
            user.CurrentSession.SessionLibrary = this;

            // Pre-read session ID (kludge)
            XmlDocument doc = new XmlDocument();
            doc.Load(baseUrlValue + "?auth=true&GetXML=true");

            string sessionId = doc.SelectSingleNode("//session").InnerText;

            // Generate URL for POST
            string postData = string.Format("GetXML=true&menu=home&subtab=subtab11&sec1={0}&sec2={1}&lang=eng&button=Login&login_prompt=true&session={2}",
                user.BarCode, user.Password, sessionId);

            XmlDocument doc2 = comm.ServerPOST(postData, user);

            if (doc2.SelectSingleNode("//security/auth").InnerText == "false") throw new Exception("Login failed");

            // Finalize the session info
            user.CurrentSession.SessionKey = sessionId;
            user.CurrentSession.LoggedIn = true;
        }

        /// <summary>
        /// Logout the user.  Currently only invalidates the session locally without sending
        /// an actual logout message.
        /// </summary>
        /// <param name="user">Patron to logout</param>
        private void Logout(Patron user)
        {
            // TODO: Invoke Logout operation on the server

            // For now...
            user.CurrentSession.Invalidate();
        }

        #region IDeserializationCallback Members

        /// <summary>
        /// Finalizes initialization when object is deserialized.
        /// Otherwise constructors are not called.
        /// </summary>
        /// <param name="sender"></param>
        public void OnDeserialization(object sender)
        {
            comm = new HorizonDataAccess(this);
            mapper = new HorizonDataMapper();
        }

        #endregion
    }
}
